Activity 结果回调

startActivityForResult() + onActivityResult() 的替代方案:先用 registerForActivityResult() 注册一个
ActivityResultLauncher,再用 launcher.launch(输入) 启动系统相机 / 相册 / 权限弹窗,结果回到注册时绑定的回调。
注意它来自 AndroidX Activity(Jetpack)

1. 新旧对比

维度 旧:startActivityForResult 新:Activity Result API
启动 startActivityForResult(intent, requestCode) launcher.launch(输入)
接收结果 onActivityResult(requestCode, resultCode, data) 里 switch 注册时绑定的回调,一个 launcher 对应一个结果
请求码 手写常量,容易撞 / 忘回收 不需要
类型 全是 int + Intent,强转靠自己 contract 定义了输入输出类型
可测试性 只能挂在 Activity 上 可以注入自己的 ActivityResultRegistry,脱离 Activity 单测
权限申请 requestPermissions() + onRequestPermissionsResult() RequestPermission() 等 contract,同一套模型

废弃发生在 AndroidX 侧:Fragment.startActivityForResult、ComponentActivity 的同名方法都标了 @Deprecated,原文理由是"类型安全(ActivityResultContract)+ 提供测试钩子 + 结果处理可以独立成可测试的类"。
平台侧的 android.app.Activity.startActivityForResult / onActivityResult 我查到的文档里没有 @Deprecated 标记(Android 15 还给它加了带 ComponentCaller 的重载),但官方推荐口径同样是迁到 Activity Result API。

2. 三步用法

class MainActivity : AppCompatActivity() {

    // 1) 注册:必须发生在 STARTED 之前(属性初始化 / onCreate 里)
    private val pickImage = registerForActivityResult(
        ActivityResultContracts.PickVisualMedia()          // 系统照片选择器
    ) { uri: Uri? ->                                       // 3) 结果回调,取消时为 null
        uri?.let { imageView.setImageURI(it) }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main)

        findViewById<Button>(R.id.btn).setOnClickListener {
            // 2) 启动
            pickImage.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
        }
    }
}

要点:

3. 常用 contract(相机 / 相册 / 权限)

contract 输入 → 输出 用途
StartActivityForResult() Intent → ActivityResult 通用:启动任意 Activity 拿结果(resultCode + data)
TakePicture() Uri → Boolean 系统相机拍照,必须给一个可写的 content URI
TakePicturePreview() 无 → Bitmap? 只要一张缩略图,不回写文件
PickVisualMedia() PickVisualMediaRequest → Uri? 选一张图片/视频(系统照片选择器,无需读存储权限)
PickMultipleVisualMedia(n) PickVisualMediaRequest → List<Uri> 选多张,n 为上限
GetContent() String(mime) → Uri? 旧的 ACTION_GET_CONTENT
GetMultipleContents() Array<String>(mime) → List<Uri> 多选内容
OpenDocument() / OpenMultipleDocuments() Array<String>(mime) → Uri? / List<Uri> SAF 打开文档,拿到的 URI 可长期持有
CreateDocument() String(建议文件名) → Uri? SAF 新建文档(mime 在构造时给)
OpenDocumentTree() Uri? → Uri? 选择整个目录
RequestPermission() String → Boolean 单个运行时权限
RequestMultiplePermissions() Array<String> → Map<String, Boolean> 多个权限,逐个给结果
StartIntentSenderForResult() IntentSenderRequest → ActivityResult 需要 IntentSender 的系统流程

4. 相机 / 相册的几个坑

5. 权限申请的迁移

private val requestCamera = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted: Boolean ->
    if (granted) openCamera() else showRationale()
}

requestCamera.launch(Manifest.permission.CAMERA)

6. 进阶:脱离 Activity 注册

7. 常见坑

  1. 在 onResume / 点击事件里注册 → IllegalStateException,必须在 STARTED 之前
  2. 想用一个 launcher 处理多种结果 → 没有 requestCode 了,需要几种结果就注册几个 launcher
  3. 拍照传 file:// → FileUriExposedException,用 FileProvider
  4. 以为 PickVisualMedia 需要 READ_MEDIA_IMAGES → 不需要,选择器自带授权
  5. launch 了不存在的系统 App → ActivityNotFoundException,隐式 Intent 要先判断
  6. 在 Fragment 里注册却指望 Activity 的 onActivityResult → 旧回调已废弃,统一用 launcher
  7. 把长生命周期对象塞进回调 → 回调会活到组件销毁,注意别捕获 Activity 造成泄漏